<?php
/**
 * Created by PhpStorm.
 * User: ari
 * Date: 12/23/2014
 * Time: 3:38 PM
 */
namespace BC\Wallet;

use BC\User\Service\TestUser;
use CPath\Data\Map\IKeyMap;
use CPath\Data\Map\IKeyMapper;
use CPath\Request\Exceptions\RequestException;
use CPath\Request\Log\ILogListener;
use CPath\UnitTest\ITestable;
use CPath\UnitTest\IUnitTestRequest;

class WalletLedger implements ITestable, ILogListener, IKeyMap
{
	const INIT = 'init';
	const OWN = 'own';
	const DISOWN = 'disown';
	const UPDATE = 'update';
	const AUTHORIZE = 'auth';
	const DEAUTHORIZE = 'deauth';
	const TRANSFER = 'transfer';
	const REVERSE = 'reverse';
	const SETTLE = 'settle';

	private $mLedger;
	/** @var ILogListener[] */
	private $mLogListeners = array();

	public function __construct(Array &$ledger) {
		$this->mLedger = &$ledger;
	}

	protected function write($args, $_args=null) {
		$this->mLedger[] = implode(' ', func_get_args());
	}

	protected function read($row) {
		return explode(' ', $row);
	}

	public function authorize($amount, $trustee) {
//		if (!in_array($owner, $this->getOwners())) {
//			throw new RequestException("Owner not found: " . $owner);}
		if (!$this->hasAvailableBalance($amount)) {
			throw new RequestException("Insufficient funds. Requested amount could not be authorized");}

		$this->write(self::AUTHORIZE, $trustee, $amount);
		$available = $this->getAvailableBalance($info);
		$this->log("User '{$trustee}' is authorized for the amount: " . $amount . ". Available: " . $available);
	}

	public function deauthorize($trustee) {
		$this->write(self::DEAUTHORIZE, $trustee);
		$this->log("User authorization has been removed: " . $trustee);
	}

	public function transfer($amount, $walletID, $trusteeID) {
		$info = array();
		$this->getAvailableBalance($info);
		$this->checkAuthority($info, $amount, $trusteeID);
		$this->write(self::TRANSFER, $amount, $walletID, $trusteeID);
		$available = $this->getAvailableBalance($info);
		if($info['auth_list'][$trusteeID] === 0) {
			$this->log("Auto-de-auth depleted authorization for trustee: " . $trusteeID, $this::VERBOSE);
			$this->deauthorize($trusteeID);
		}
		$this->log("Funds transferred to {$walletID}: " . $amount . ". Available: " . $available);
	}

	public function reverse($amount, $walletID, $trustee) {
		$info = array();
		$this->getAvailableBalance($info);
		$this->checkAuthority($info, $amount, $trustee);
		$this->write(self::REVERSE, $amount, $walletID, $trustee);
		$available = $this->getAvailableBalance($info);
		if($info['auth_list'][$trustee] === 0) {
			$this->log("Auto-de-auth depleted authorization for trustee: " . $trustee, $this::VERBOSE);
			$this->deauthorize($trustee);
		}
		$this->log("Funds transfer to '{$walletID}' has been reversed by the amount : " . $amount . ". Available: " . $available);
	}

	public function getTransferBalanceForWallet($walletID) {
		$info = array();
		$this->getAvailableBalance($info);
		if(!isset($info['transfers'][$walletID]))
			return false;
		return $info['transfers'][$walletID];
	}

	public function settle($walletID) {
		$balance = $this->getTransferBalanceForWallet($walletID);
		if(!$balance)
			throw new \InvalidArgumentException("Wallet does not have a pending transfer: " . $walletID);
		$this->write(self::SETTLE, $walletID, $balance);
		$available = $this->getAvailableBalance($info);
		$this->log("Funds transfer to '{$walletID}' has been settled: " . $balance . ". Available: " . $available);
	}

	protected function checkAuthority($info, $amount, $trustee) {
		if(!isset($info['auth_list'][$trustee]))
			throw new \Exception("Trustee is not authorized: " . $trustee);
		$authorizedAmount = $info['auth_list'][$trustee];
		if($amount > $authorizedAmount)
			throw new RequestException("Authority does not cover this amount");
	}

	public function initLedger($walletAddress, $ownerFingerprints) {
		$this->mLedger          = array();
		$this->write(self::INIT, $walletAddress);
		foreach((array)$ownerFingerprints as $ownerFingerprint)
			$this->write(self::OWN, $ownerFingerprint);
		$available = $this->getAvailableBalance($info);
		$this->log("Wallet '{$walletAddress}' has been initialized with owner: " . $ownerFingerprints . ". Available Balance: " . $available);
	}

	public function updateBalance($balance, $received, $sent) {
		$info = array();
		$this->getAvailableBalance($info);
		if($info['balance'] === $balance)
			return false;
		if($info['balance'] > $balance)
			throw new \InvalidArgumentException("balance went down!");

		$this->write(self::UPDATE, $balance, $received, $sent);
		$this->getAvailableBalance($info);
		$this->log("Wallet balance has been updated: " . $balance);
		return true;
	}

	public function addOwner($ownerFingerprint) {
		$this->write(self::OWN, $ownerFingerprint);
	}

	public function removeOwner($ownerFingerprint) {
		$this->write(self::DISOWN, $ownerFingerprint);
	}

	public function getOwners() {
		$owners = array();
		foreach ($this->mLedger as $row) {
			$arg = $this->read($row);
			switch (str_replace('p', 'o', $arg[0])) {
				case static::OWN:
					$newOwner = $arg[1];
					if(in_array($newOwner, $owners))
						throw new \Exception("Owner already owns wallet: " . $newOwner);
					$owners[] = $newOwner;
					break;

				case static::DISOWN:
					$oldOwner = $arg[1];
					if(!in_array($oldOwner, $owners))
						throw new \Exception("Non-owner cannot disown wallet: " . $oldOwner);
					$owners = array_diff($owners, array($oldOwner));
					break;
			}
		}

		if(!$owners)
			throw new \Exception("No wallet owners found. tragic.");
		return $owners;
	}


	public function hasAvailableBalance($amount) {
		$availableAmount = $this->getAvailableBalance();
		return $amount < $availableAmount;
	}


	public function getTotalBalance() {
		$info = array();
		$this->getAvailableBalance($info);

		return $info['balance'];
	}

	/**
	 * @return String
	 * @throws \Exception
	 */
	function getWalletAddress() {
		foreach ($this->mLedger as $row) {
			$arg = $this->read($row);
			switch ($arg[0]) {
				case static::INIT:
					return $arg[1];
			}
		}

		throw new \Exception("No wallet address found in ledger");
	}

	function getAvailableBalance(Array &$info = null) {
		$info = array(
			'balance'             => 0,
			'received_by_address' => 0,
			'sent_by_address'     => 0,
			'auth_list'           => array(),
			'transfers'           => array(),
			'authorized'          => 0,
			'available'           => 0,
			'transfer'        => 0,
			'address'             => null,
		);
		foreach ($this->mLedger as $row) {
			$arg = $this->read($row);
			switch ($arg[0]) {
				case static::INIT:
					$info['address'] = $arg[1];
					break;

				case static::AUTHORIZE:
					list(, $trusteeID, $amount) = $arg;
					$amount = intval($amount);
					if(isset($info['auth_list'][$trusteeID]))
						throw new \InvalidArgumentException("Trustee already authorized for " . var_export($info['auth_list'][$trusteeID], true) . ": " . $trusteeID);
					$info['auth_list'][$trusteeID] = $amount;
					$info['authorized'] += $amount;
					break;

				case static::DEAUTHORIZE:
					list(, $trusteeID) = $arg;
					$amount = $info['auth_list'][$trusteeID];
					$info['authorized'] -= $amount;
					unset($info['auth_list'][$trusteeID]);
					if($info['authorized'] < 0)
						throw new \InvalidArgumentException("Negative authorized balance: " . $trusteeID);

					break;

				case static::TRANSFER:
					list(, $amount, $walletID, $trusteeID) = $arg;
					if(!isset($info['auth_list'][$trusteeID]))
						throw new \Exception("Trustee is not authorized: " . $trusteeID);

					$this->checkAuthority($info, $amount, $trusteeID);
//					$amount = $info['auth_list'][$trustee];

					$info['authorized'] -= $amount;
					if($info['authorized'] < 0)
						throw new \InvalidArgumentException("Negative authorized balance: " . $trusteeID);

					$info['auth_list'][$trusteeID] -= $amount;
					if($info['auth_list'][$trusteeID] < 0)
						throw new \InvalidArgumentException("Negative authorized balance: " . $trusteeID);

					$info['transfer'] += $amount;
					if(!isset($info['transfers'][$walletID]))
						$info['transfers'][$walletID] = 0;
					$info['transfers'][$walletID] += $amount;
					break;


				case static::REVERSE:
					list(, $amount, $walletID, $trusteeID) = $arg;
					if(!isset($info['auth_list'][$trusteeID]))
						throw new \Exception("Trustee is not authorized: " . $trusteeID);
					if(!isset($info['transfers'][$walletID]))
						throw new \Exception("Transfer is not pending for wallet: " . $walletID);
					if($amount > $info['transfers'][$walletID])
						throw new \Exception("Transfer Reversal amount is greater than pending tansfer amount: {$amount} > "  . $info['transfers'][$walletID]);

					$info['authorized'] += $amount;
					$info['auth_list'][$trusteeID] += $amount;

					$info['transfer'] -= $amount;
					$info['transfers'][$walletID] -= $amount;
					if($info['transfers'][$walletID] === 0)
						unset($info['transfers'][$walletID]);
					break;

				case static::SETTLE:
					list(, $walletID, $balance) = $arg;
					$balance = intval($balance);
					if($info['transfers'][$walletID] !== $balance)
						throw new \Exception("Settle Balance Mismatch: {$balance} !== " . $info['transfers'][$walletID]);
					unset($info['transfers'][$walletID]);

					$info['transfer'] -= $balance;
					$info['sent_by_address'] += $balance;
					$info['balance'] -= $balance;
					break;

				case static::UPDATE:
					// TODO: Whazzis?
//					if ($info['received_by_address'] && $info['received_by_address'] >= $arg[1]) {
//						throw new \InvalidArgumentException("Invalid balance update (received) {$info['received_by_address']} => " . $arg[1]); }
					$info['balance'] = intval($arg[1]);
					$info['received_by_address'] = intval($arg[2]);
					$info['sent_by_address'] = intval($arg[3]);
					break;
			}
		}

		$balance = $info['received_by_address'] - $info['sent_by_address'];
		if ($balance !== $info['balance'])
			throw new \InvalidArgumentException("Balance mismatch {$info['received_by_address']} - {$info['sent_by_address']} !== {$info['balance']}");
		$info['available'] = $info['balance'] - $info['authorized'] - $info['transfer'];

		return $info['available'];
	}


	/**
	 * Add a log entry
	 * @param mixed $msg The log message
	 * @param int $flags [optional] log flags
	 * @return int the number of listeners that processed the log entry
	 */
	function log($msg, $flags = 0) {
		$c = 0;
		foreach($this->mLogListeners as $Log)
			$c += $Log->log($msg, $flags);
		return $c;
	}

	/**
	 * Add a log listener callback
	 * @param ILogListener $Listener
	 * @return void
	 */
	function addLogListener(ILogListener $Listener) {
		if(!in_array($Listener, $this->mLogListeners))
			$this->mLogListeners[] = $Listener;
	}

//	/**
//	 * Map sequential data to the map
//	 * @param ISequenceMapper $Map
//	 */
//	function mapSequence(ISequenceMapper $Map) {
//		foreach($this->mLedger as $ledger) {
//			$Map->mapNext($ledger);
//		}
//	}

	/**
	 * Map data to the key map
	 * @param IKeyMapper $Map the map inst to add data to
	 * @internal param \CPath\Request\IRequest $Request
	 * @internal param \CPath\Request\IRequest $Request
	 * @return void
	 */
	function mapKeys(IKeyMapper $Map) {
		$Map->map('address', $this->getWalletAddress());
		$Map->map('balance', $this->getTotalBalance());
		$Map->map('available', $this->getAvailableBalance());
	}

	// Static

	/**
	 * Perform a unit test
	 * @param IUnitTestRequest $Test the unit test request inst for this test session
	 * @return void
	 * @test --disable 0
	 * Note: Use doctag 'test' with '--disable 1' to have this ITestable class skipped during a build
	 */
	static function handleStaticUnitTest(IUnitTestRequest $Test) {
		$TestUser = new TestUser($Test, 'wallet-ledger');
		$array = array();
		$Ledger = new WalletLedger($array);
		$Ledger->addLogListener($Test);

		$trustee = 'trustee';
		$my_address = 'my_address';
		$target_address = 'target_address';

		$sent = 70000;
		$received = 90000;
		$auth = 5000;
		$balance = $received - $sent;

		$transfer = 2933;
		$reverse = 385;

		$Ledger->initLedger($my_address, $TestUser->getFingerprint());

		$Ledger->updateBalance($balance, $received, $sent);

		$Ledger->authorize($auth, $trustee);
		$Test->assertEqual($balance - $auth, $Ledger->getAvailableBalance());

		$Ledger->deauthorize($trustee);
		$Test->assertEqual($balance, $Ledger->getAvailableBalance());



		$Ledger->authorize($auth, $trustee);
		$Test->assertEqual($balance - $auth, $Ledger->getAvailableBalance());
		$Test->assertEqual($balance, $Ledger->getTotalBalance());

		$Ledger->transfer($transfer, $target_address, $trustee);
		$Test->assertEqual($balance - $auth, $Ledger->getAvailableBalance());

		$Ledger->reverse($reverse, $target_address, $trustee);
		$Test->assertEqual($balance - $auth, $Ledger->getAvailableBalance());

//		$unsettled = $Ledger->getTransferBalanceForWallet($target_address);
		$Ledger->settle($target_address);
		$Test->assertEqual($balance - $transfer + $reverse, $Ledger->getTotalBalance());

		$Test->assertEqual($my_address, $Ledger->getWalletAddress());
	}
}